# -*- coding: utf-8 -*-
"""
make_4_4_full_txt_all_fc.py

只聚焦“附件2”的 4.4 章节，基于 4.3 守卫（/guards_4_3_from_attachment2.horn）：
- 生成**全量/到收敛**的反例集合（单守卫/多守卫按 4.4 的“守卫集合联立”展开到覆盖收敛）。
- 输出两份**行对齐**文件（逐行一条用例）：
    1) labels.txt   —— 人类可读标签：严格格式
       pid=<d> len=<d> fc=<d> start=<d> qty=<d> bc=<d> vbytes=<d>
       （不要担心换行：用文件换行符写入，并非手动拼接 "\n"）
    2) hex.txt      —— 对应整帧的纯16进制（MBAP+PDU，空格分隔），与 labels.txt **行号一一对应**
- 所有控制参数都在脚本“内部常量”里定义（无外部参数）。
- 默认使用**全量**边界（可能极大）。如需收缩，请只改“内部常量”，不要改接口。

覆盖的功能码（均为到收敛的全量）：
  - 0x01 Read Coils（数量上限 2000）
  - 0x02 Read Discrete Inputs（数量上限 2000）
  - 0x03 Read Holding Registers（数量上限 125）
  - 0x04 Read Input Registers（数量上限 125）
  - 0x0F Write Multiple Coils（数量上限 1968；bc=ceil(q/8)）
  - 0x17 Read/Write Multiple Registers（读上限 125；写上限 123；bc=2*w_qty；vbytes≥2*w_qty）
  - 0x10 Write Multiple Registers（数量上限 123；bc=2*qty；vbytes≥2*qty）——保留（之前已有），这里也全量

另外保留通用的 MBAP / 传输层守卫反例：
  - PID≠0（pid=1..65535）
  - LEN=0
  - PDU_EMPTY（PDU 长度=0 ⇒ len=1）
  - FC_UNSUPPORTED：fc ∉ SupportedFC（从 horn 文件解析）

注意：
  1) 取值域遵循 Modbus/TCP 字段宽度（16位/8位），MBAP.len ≤ 65535。
  2) 对于“地址越界”的全量枚举，规模极大（起码数百万到千万级）。如需收缩可改 START_MAX。
  3) 脚本只输出 7 个键：pid,len,fc,start,qty,bc,vbytes；其余字段（addr/value/and/or…）
     已内嵌在 PDU 构造里；若你也要它们进入标签，按同样方式扩一列即可。
"""

from pathlib import Path
import re
import sys

# ========================= 内部常量（如需收缩规模，只改这里） =========================

# 输出位置（相对脚本目录）
OUT_DIR = Path("./out_4_4_full")
LABELS_PATH = OUT_DIR / "labels.txt"
HEX_PATH    = OUT_DIR / "hex.txt"

# horn 规则位置（可放同目录；找不到则使用默认值）
HORN_PATH = Path("./guards_4_3_from_attachment2.horn")

# 字段取值上界/域（全量模式，不建议改）：
PID_MIN, PID_MAX = 1, 0xFFFF
FC_DOMAIN = list(range(0, 256))

# 读/写数量上限（遵循 Modbus 规范）
QMAX_R_COILS = 2000   # FC=0x01/0x02
QMAX_R_REGS  = 125    # FC=0x03/0x04
QMAX_W_COILS = 1968   # FC=0x0F
QMAX_W_REGS  = 123    # FC=0x10/0x17 写侧

# FC=0x10/0x17 的“值字节”长度受 MBAP.len（16位）约束。
# 0x10: MBAP.len = 1 + (1+2+2+1 + vbytes) = 7 + vbytes ⇒ vbytes ≤ 65535-7
# 若保持自洽 vbytes=2*qty，则 2*qty ≤ 65535-7 ⇒ qty ≤ 32764（足够覆盖 qty>QMAX_W_REGS 的所有上溢）
QTY_UPPER_FC10 = 32764
QTY_UPPER_FC17_W = 32764  # 写侧同理（若以 vbytes=2*w_qty 自洽）

# 地址空间（16位）
START_MIN, START_MAX = 0, 0xFFFF

# ========================= 工具函数 =========================

def u16(x: int) -> bytes:
    return int(x & 0xFFFF).to_bytes(2, "big", signed=False)

def u8(x: int) -> bytes:
    return int(x & 0xFF).to_bytes(1, "big", signed=False)

def hexline(b: bytes) -> str:
    return " ".join(f"{v:02X}" for v in b)

def write_line(fh, line: str):
    fh.write(line)
    fh.write("\n")

# ========================= 4.3 常量解析（SupportedFC, REG_MAX） =========================

def parse_supported_and_regmax_from_horn(horn_path: Path):
    default_supported = [1,2,3,4,5,6,15,16,22,23]
    default_regmax = 2000
    if not horn_path.exists():
        return sorted(default_supported), default_regmax
    text = horn_path.read_text(encoding="utf-8", errors="ignore")
    fcs = [int(m.group(1)) for m in re.finditer(r"SupportedFC\((\d+)\)\.", text)]
    if not fcs:
        fcs = default_supported[:]
    m = re.search(r"AddrOK\(s,q\)\s*:-\s*s\s*\+\s*q\s*=\<\s*(\d+)\.", text)
    regmax = int(m.group(1)) if m else default_regmax
    return sorted(set(fcs)), regmax

# ========================= PDU/Frame 构造 =========================

def build_pdu(fc:int, *,
              start=0, qty=1, addr=0, value=1, bc=0, vbytes=0, andm=0xF0F0, orm=0x0F0F,
              r_start=0, r_qty=1, w_start=0, w_qty=1, force_len:int=None) -> bytes:
    """按 FC 形状构造 PDU；force_len 覆盖“短PDU”情形"""
    if force_len is not None:
        # 短 PDU：仅 fc + 填充
        plen = max(0, int(force_len))
        return (u8(fc) + bytes(max(0, plen-1)))[:plen]

    if fc in (1,2,3,4):  # read (start, qty)
        return u8(fc) + u16(start) + u16(qty)

    if fc in (5,6):      # write single (addr, value) —— 4.4不输出它的标签字段，但仍能构造
        return u8(fc) + u16(addr) + u16(value)

    if fc == 15:         # write multiple coils (start, qty, bc, values)
        return u8(fc) + u16(start) + u16(qty) + u8(bc) + bytes(max(0, vbytes))

    if fc == 16:         # write multiple regs (start, qty, bc, values)
        return u8(fc) + u16(start) + u16(qty) + u8(bc) + bytes(max(0, vbytes))

    if fc == 22:         # mask write register (addr, and, or)
        return u8(fc) + u16(addr) + u16(andm) + u16(orm)

    if fc == 23:         # read/write regs (r_start, r_qty, w_start, w_qty, bc, values)
        return (u8(fc) + u16(r_start) + u16(r_qty) + u16(w_start) + u16(w_qty) + u8(bc)
                + bytes(max(0, vbytes)))

    # Unsupported：仅功能码
    return u8(fc)

def build_frame(pid:int, mbap_len:int, uid:int, pdu:bytes) -> bytes:
    """若 mbap_len < 0，则按自洽（1+len(pdu)）填入。"""
    if mbap_len < 0:
        mbap_len = 1 + len(pdu)
    return u16(0x0001) + u16(pid & 0xFFFF) + u16(mbap_len & 0xFFFF) + u8(uid & 0xFF) + pdu

# ========================= 主过程（全量到收敛） =========================

def main():
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    supported_fc, REG_MAX = parse_supported_and_regmax_from_horn(HORN_PATH)
    supported_set = set(supported_fc)

    with LABELS_PATH.open("w", encoding="utf-8") as flab, HEX_PATH.open("w", encoding="utf-8") as fhex:

        # ----------------------------------------------------------------------
        # 通用守卫反例：PID≠0 / LEN=0 / PDU_EMPTY / FC_UNSUPPORTED
        # ----------------------------------------------------------------------
        # 选定一个合规 PDU（FC=16, qty=1, bc=2, vbytes=2）
        pdu_ok = build_pdu(16, start=0, qty=1, bc=2, vbytes=2)

        # PID ≠ 0：pid=1..65535
        mbap_len_ok = 1 + len(pdu_ok)
        for pid in range(PID_MIN, PID_MAX + 1):
            # 标签
            write_line(flab, f"pid={pid} len={mbap_len_ok} fc=16 start=0 qty=1 bc=2 vbytes=2")
            # HEX
            write_line(fhex, hexline(build_frame(pid, mbap_len_ok, 1, pdu_ok)))

        # LEN=0：无 PDU
        write_line(flab, f"pid=0 len=0 fc=16 start=0 qty=1 bc=2 vbytes=2")
        write_line(fhex, hexline(build_frame(0, 0, 1, b"")))

        # PDU_EMPTY：PDU 长度=0（短PDU），len=1
        pdu_empty = build_pdu(16, force_len=0)
        write_line(flab, f"pid=0 len=1 fc=16 start=0 qty=1 bc=2 vbytes=2")
        write_line(fhex, hexline(build_frame(0, -1, 1, pdu_empty)))

        # FC_UNSUPPORTED：fc ∉ SupportedFC（逐个枚举）
        for fc in FC_DOMAIN:
            if fc in supported_set:
                continue
            pdu = build_pdu(fc)
            write_line(flab, f"pid=0 len={1+len(pdu)} fc={fc} start=0 qty=1 bc=0 vbytes=0")
            write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))

        # ----------------------------------------------------------------------
        # 各 FC 的“短 PDU”违规（最小长度不足）
        # 0x01..0x04: need=5; 0x0F: need=6; 0x10: need=6; 0x17: need=11
        # ----------------------------------------------------------------------
        short_need = {1:5, 2:5, 3:5, 4:5, 15:6, 16:6, 23:11}
        for fc, need in short_need.items():
            for plen in range(0, need):  # 0..need-1
                pdu = build_pdu(fc, force_len=plen)
                write_line(flab, f"pid=0 len={1+len(pdu)} fc={fc} start=0 qty=1 bc=0 vbytes=0")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))

        # ----------------------------------------------------------------------
        # FC=0x01 Read Coils：1≤qty≤2000，AddrOK(start+qty≤REG_MAX)
        # 反例：
        #  - qty 越界：qty=0 以及 qty≥2001..65535（start 固定 0）
        #  - 地址越界：qty∈[1..2000], start∈[REG_MAX-qty+1 .. 65535]
        # ----------------------------------------------------------------------
        fc = 1
        # qty OOB
        write_line(flab, f"pid=0 len=6 fc=1 start=0 qty=0 bc=0 vbytes=0")
        write_line(fhex, hexline(build_frame(0, -1, 1, build_pdu(fc, start=0, qty=0))))
        for qty in range(QMAX_R_COILS+1, 0x10000):
            pdu = build_pdu(fc, start=0, qty=qty)
            write_line(flab, f"pid=0 len=6 fc=1 start=0 qty={qty} bc=0 vbytes=0")
            write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # addr OOB
        for qty in range(1, QMAX_R_COILS+1):
            smin = REG_MAX - qty + 1
            if smin < 0: smin = 0
            for start in range(smin, START_MAX+1):
                pdu = build_pdu(fc, start=start, qty=qty)
                write_line(flab, f"pid=0 len=6 fc=1 start={start} qty={qty} bc=0 vbytes=0")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))

        # ----------------------------------------------------------------------
        # FC=0x02 Read Discrete Inputs：同 0x01
        # ----------------------------------------------------------------------
        fc = 2
        write_line(flab, f"pid=0 len=6 fc=2 start=0 qty=0 bc=0 vbytes=0")
        write_line(fhex, hexline(build_frame(0, -1, 1, build_pdu(fc, start=0, qty=0))))
        for qty in range(QMAX_R_COILS+1, 0x10000):
            pdu = build_pdu(fc, start=0, qty=qty)
            write_line(flab, f"pid=0 len=6 fc=2 start=0 qty={qty} bc=0 vbytes=0")
            write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        for qty in range(1, QMAX_R_COILS+1):
            smin = REG_MAX - qty + 1
            if smin < 0: smin = 0
            for start in range(smin, START_MAX+1):
                pdu = build_pdu(fc, start=start, qty=qty)
                write_line(flab, f"pid=0 len=6 fc=2 start={start} qty={qty} bc=0 vbytes=0")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))

        # ----------------------------------------------------------------------
        # FC=0x03 Read Holding Registers：1≤qty≤125；AddrOK
        # 反例：
        #  - qty OOB：qty=0 及 qty≥126..65535（start=0）
        #  - addr OOB：qty∈[1..125], start∈[REG_MAX-qty+1 .. 65535]
        # ----------------------------------------------------------------------
        fc = 3
        write_line(flab, f"pid=0 len=6 fc=3 start=0 qty=0 bc=0 vbytes=0")
        write_line(fhex, hexline(build_frame(0, -1, 1, build_pdu(fc, start=0, qty=0))))
        for qty in range(QMAX_R_REGS+1, 0x10000):
            pdu = build_pdu(fc, start=0, qty=qty)
            write_line(flab, f"pid=0 len=6 fc=3 start=0 qty={qty} bc=0 vbytes=0")
            write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        for qty in range(1, QMAX_R_REGS+1):
            smin = REG_MAX - qty + 1
            if smin < 0: smin = 0
            for start in range(smin, START_MAX+1):
                pdu = build_pdu(fc, start=start, qty=qty)
                write_line(flab, f"pid=0 len=6 fc=3 start={start} qty={qty} bc=0 vbytes=0")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))

        # ----------------------------------------------------------------------
        # FC=0x04 Read Input Registers：同 0x03
        # ----------------------------------------------------------------------
        fc = 4
        write_line(flab, f"pid=0 len=6 fc=4 start=0 qty=0 bc=0 vbytes=0")
        write_line(fhex, hexline(build_frame(0, -1, 1, build_pdu(fc, start=0, qty=0))))
        for qty in range(QMAX_R_REGS+1, 0x10000):
            pdu = build_pdu(fc, start=0, qty=qty)
            write_line(flab, f"pid=0 len=6 fc=4 start=0 qty={qty} bc=0 vbytes=0")
            write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        for qty in range(1, QMAX_R_REGS+1):
            smin = REG_MAX - qty + 1
            if smin < 0: smin = 0
            for start in range(smin, START_MAX+1):
                pdu = build_pdu(fc, start=start, qty=qty)
                write_line(flab, f"pid=0 len=6 fc=4 start={start} qty={qty} bc=0 vbytes=0")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))

        # ----------------------------------------------------------------------
        # FC=0x0F Write Multiple Coils：1≤qty≤1968；bc=ceil(q/8)；AddrOK
        # 反例：
        #  - qty OOB：qty=0 及 qty≥1969..65535（bc/vbytes 设置为自洽或最小）
        #  - bc mismatch：qty∈[1..1968]，bc∈[0..255]\{ceil(q/8)}（vbytes=ceil(q/8)）
        #  - addr OOB：qty∈[1..1968]，start∈[REG_MAX-qty+1 .. 65535]（bc,vbytes自洽）
        # ----------------------------------------------------------------------
        fc = 15
        # qty OOB
        write_line(flab, f"pid=0 len=7 fc=15 start=0 qty=0 bc=0 vbytes=0")
        write_line(fhex, hexline(build_frame(0, -1, 1, build_pdu(fc, start=0, qty=0, bc=0, vbytes=0))))
        for qty in range(QMAX_W_COILS+1, 0x10000):
            # 为避免 MBAP.len 爆表，取最小 vbytes（这里选0；不要求与 qty 自洽，因为守卫只看 qty）
            pdu = build_pdu(fc, start=0, qty=qty, bc=0, vbytes=0)
            write_line(flab, f"pid=0 len={1+len(pdu)} fc=15 start=0 qty={qty} bc=0 vbytes=0")
            write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # bc mismatch
        for qty in range(1, QMAX_W_COILS+1):
            expected = (qty + 7) // 8
            for bc in range(0, 256):
                if bc == expected:
                    continue
                pdu = build_pdu(fc, start=0, qty=qty, bc=bc, vbytes=expected)
                write_line(flab, f"pid=0 len={1+len(pdu)} fc=15 start=0 qty={qty} bc={bc} vbytes={expected}")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # addr OOB
        for qty in range(1, QMAX_W_COILS+1):
            expected = (qty + 7) // 8
            smin = REG_MAX - qty + 1
            if smin < 0: smin = 0
            for start in range(smin, START_MAX+1):
                pdu = build_pdu(fc, start=start, qty=qty, bc=expected, vbytes=expected)
                write_line(flab, f"pid=0 len={1+len(pdu)} fc=15 start={start} qty={qty} bc={expected} vbytes={expected}")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))

        # ----------------------------------------------------------------------
        # FC=0x10 Write Multiple Registers：1≤qty≤123；bc=2*qty；vbytes≥2*qty；AddrOK
        # 反例：
        #  - qty OOB：qty=0 及 qty≥124..32764（保 len≤65535；bc 可随意，或取(2*qty)&0xFF）
        #  - bc mismatch：qty∈[1..123]，bc∈[0..255]\{2*qty}（vbytes=2*qty）
        #  - vbytes 短：qty∈[1..123]，vbytes ∈ [0..(2*qty-1)]（bc=2*qty）
        #  - addr OOB：qty∈[1..123]，start∈[REG_MAX-qty+1 .. 65535]（bc,vbytes自洽）
        # ----------------------------------------------------------------------
        fc = 16
        # qty OOB（上溢到 32764）
        write_line(flab, f"pid=0 len=7 fc=16 start=0 qty=0 bc=0 vbytes=0")
        write_line(fhex, hexline(build_frame(0, -1, 1, build_pdu(fc, start=0, qty=0, bc=0, vbytes=0))))
        for qty in range(QMAX_W_REGS+1, QTY_UPPER_FC10+1):
            bc = (2*qty) & 0xFF
            # vbytes 可以取 0（仍满足“qty 越界”守卫，且保证 MBAP.len 不爆表）
            pdu = build_pdu(fc, start=0, qty=qty, bc=bc, vbytes=0)
            write_line(flab, f"pid=0 len={1+len(pdu)} fc=16 start=0 qty={qty} bc={bc} vbytes=0")
            write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # bc mismatch
        for qty in range(1, QMAX_W_REGS+1):
            expected = (2*qty) & 0xFF
            vbytes = 2*qty
            for bc in range(0, 256):
                if bc == expected:
                    continue
                pdu = build_pdu(fc, start=0, qty=qty, bc=bc, vbytes=vbytes)
                write_line(flab, f"pid=0 len={1+len(pdu)} fc=16 start=0 qty={qty} bc={bc} vbytes={vbytes}")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # vbytes 短
        for qty in range(1, QMAX_W_REGS+1):
            bc = (2*qty) & 0xFF
            for vbytes in range(0, 2*qty):
                pdu = build_pdu(fc, start=0, qty=qty, bc=bc, vbytes=vbytes)
                write_line(flab, f"pid=0 len={1+len(pdu)} fc=16 start=0 qty={qty} bc={bc} vbytes={vbytes}")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # addr OOB
        for qty in range(1, QMAX_W_REGS+1):
            bc = (2*qty) & 0xFF
            vbytes = 2*qty
            smin = REG_MAX - qty + 1
            if smin < 0: smin = 0
            for start in range(smin, START_MAX+1):
                pdu = build_pdu(fc, start=start, qty=qty, bc=bc, vbytes=vbytes)
                write_line(flab, f"pid=0 len={1+len(pdu)} fc=16 start={start} qty={qty} bc={bc} vbytes={vbytes}")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))

        # ----------------------------------------------------------------------
        # FC=0x17 Read/Write Multiple Registers：
        #   读：1≤r_qty≤125；写：1≤w_qty≤123；bc=2*w_qty；vbytes≥2*w_qty；两段 AddrOK
        # 反例：
        #  - r_qty OOB：r_qty=0 及 r_qty≥126..65535（w_qty 合规最小 1；bc/vbytes 自洽或最小）
        #  - w_qty OOB：w_qty=0 及 w_qty≥124..32764（bc/vbytes 最小或自洽）
        #  - bc mismatch：bc∈[0..255]\{2*w_qty}（r_qty/w_qty 合规；vbytes=2*w_qty）
        #  - r_addr OOB：r_start+r_qty > REG_MAX（w 部分合规）
        #  - w_addr OOB：w_start+w_qty > REG_MAX（r 部分合规；bc,vbytes自洽）
        #  - vbytes 短：vbytes ∈ [0..(2*w_qty-1)]（r/w 合规；bc=2*w_qty）
        # ----------------------------------------------------------------------
        fc = 23
        # r_qty OOB
        # r_qty=0，w_qty=1（最小合规），bc=2，vbytes=2
        pdu = build_pdu(fc, r_start=0, r_qty=0, w_start=0, w_qty=1, bc=2, vbytes=2)
        write_line(flab, f"pid=0 len={1+len(pdu)} fc=23 start=0 qty=0 bc=2 vbytes=2")
        write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        for rq in range(QMAX_R_REGS+1, 0x10000):
            pdu = build_pdu(fc, r_start=0, r_qty=rq, w_start=0, w_qty=1, bc=2, vbytes=2)
            write_line(flab, f"pid=0 len={1+len(pdu)} fc=23 start=0 qty={rq} bc=2 vbytes=2")
            write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # w_qty OOB（上溢到 32764）
        # w_qty=0（r_qty=1 合规），bc/vbytes 可 0
        pdu = build_pdu(fc, r_start=0, r_qty=1, w_start=0, w_qty=0, bc=0, vbytes=0)
        write_line(flab, f"pid=0 len={1+len(pdu)} fc=23 start=0 qty=0 bc=0 vbytes=0")
        write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        for wq in range(QMAX_W_REGS+1, QTY_UPPER_FC17_W+1):
            bc = (2*wq) & 0xFF
            # 为防止 MBAP.len 爆表，vbytes 取 0（仍满足“写数量越界”守卫）
            pdu = build_pdu(fc, r_start=0, r_qty=1, w_start=0, w_qty=wq, bc=bc, vbytes=0)
            write_line(flab, f"pid=0 len={1+len(pdu)} fc=23 start=0 qty={wq} bc={bc} vbytes=0")
            write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # bc mismatch（r/w 合规，vbytes=2*w_qty）
        for wq in range(1, QMAX_W_REGS+1):
            bc_expected = (2*wq) & 0xFF
            vbytes = 2*wq
            for bc in range(0, 256):
                if bc == bc_expected:
                    continue
                pdu = build_pdu(fc, r_start=0, r_qty=1, w_start=0, w_qty=wq, bc=bc, vbytes=vbytes)
                write_line(flab, f"pid=0 len={1+len(pdu)} fc=23 start=0 qty={wq} bc={bc} vbytes={vbytes}")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # r_addr OOB（r 越界；w 合规最小）
        for rq in range(1, QMAX_R_REGS+1):
            smin = REG_MAX - rq + 1
            if smin < 0: smin = 0
            for rstart in range(smin, START_MAX+1):
                pdu = build_pdu(fc, r_start=rstart, r_qty=rq, w_start=0, w_qty=1, bc=2, vbytes=2)
                write_line(flab, f"pid=0 len={1+len(pdu)} fc=23 start={rstart} qty={rq} bc=2 vbytes=2")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # w_addr OOB（w 越界；r 合规最小；bc/vbytes 自洽）
        for wq in range(1, QMAX_W_REGS+1):
            bc_expected = (2*wq) & 0xFF
            vbytes = 2*wq
            smin = REG_MAX - wq + 1
            if smin < 0: smin = 0
            for wstart in range(smin, START_MAX+1):
                pdu = build_pdu(fc, r_start=0, r_qty=1, w_start=wstart, w_qty=wq, bc=bc_expected, vbytes=vbytes)
                write_line(flab, f"pid=0 len={1+len(pdu)} fc=23 start={wstart} qty={wq} bc={bc_expected} vbytes={vbytes}")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))
        # vbytes 短（r/w 合规；bc=2*w_qty；vbytes ∈ [0..2*w_qty-1]）
        for wq in range(1, QMAX_W_REGS+1):
            bc_expected = (2*wq) & 0xFF
            for vbytes in range(0, 2*wq):
                pdu = build_pdu(fc, r_start=0, r_qty=1, w_start=0, w_qty=wq, bc=bc_expected, vbytes=vbytes)
                write_line(flab, f"pid=0 len={1+len(pdu)} fc=23 start=0 qty={wq} bc={bc_expected} vbytes={vbytes}")
                write_line(fhex, hexline(build_frame(0, -1, 1, pdu)))

    # 结束
    print(f"[OK] labels: {LABELS_PATH}")
    print(f"[OK] hex   : {HEX_PATH}")
    print(f"[INFO] SupportedFC: {supported_fc}, REG_MAX={REG_MAX}")
    print("[WARN] 这是全量到收敛，输出会极大。若需收缩，上面“内部常量”里可适度减小 START_MAX / 上界。")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n[INTERRUPTED] 手动终止。", file=sys.stderr)
